#include "stdafx.h"
#include "PythonApiHelpers.h"
#include "HttpException.h"
#include "isapi_python.h"
#include "WsgiErrorStream.h"
#include "WsgiInputStream.h"
#include "HttpContext.h"
#include "MD5HashProvider.h"

using namespace std;
using namespace boost;
using namespace ISAPI_Python::Win32Helpers;

HttpResponse::HttpResponse() throw() :
	Status ( string("200 OK") ),
	HeadersSent ( FALSE ),
	Headers ( tr1::shared_ptr<NameValueCollection>(new NameValueCollection) )
{
	(*Headers)["content-type"] = string_ptr(new string("text/plain"));

	GILock lock; //Acquire the GIL.
	PythonEnvironment = PyDict_New();
}

void HttpResponse::BuildWsgiEnvironment() {
	//Since there's no way to enumerate the server variables,
	// I'll manually copy them into the Wsgi environment dictionary.
	SetItem("ALL_HTTP");
	SetItem("ALL_RAW");
	SetItem("APP_POOL_ID");
	SetItem("APPL_MD_PATH");
	SetItem("APPL_PHYSICAL_PATH");
	SetItem("AUTH_PASSWORD");
	SetItem("AUTH_TYPE");
	SetItem("CACHE_URL");
	SetItem("CERT_COOKIE");
	SetItem("CERT_FLAGS");
	SetItem("CERT_ISSUER");
	SetItem("CERT_KEYSIZE");
	SetItem("CERT_SECRETKEYSIZE");
	SetItem("CERT_SERIALNUMBER");
	SetItem("CERT_SERVER_ISSUER");
	SetItem("CERT_SERVER_SUBJECT");
	SetItem("CERT_SUBJECT");
	SetItem("CONTENT_LENGTH");
	SetItem("CONTENT_TYPE");
	SetItem("GATEWAY_INTERFACE");
	SetItem("HTTPS");
	SetItem("HTTPS_KEYSIZE");
	SetItem("HTTPS_SECRETKEYSIZE");
	SetItem("HTTPS_SERVER_ISSUER");
	SetItem("HTTPS_SERVER_SUBJECT");
	SetItem("INSTANCE_ID");
	SetItem("INSTANCE_META_PATH");
	SetItem("LOCAL_ADDR");
	SetItem("LOGON_USER");
	SetItem("PATH_INFO");
	SetItem("PATH_TRANSLATED");
	SetItem("QUERY_STRING");
	SetItem("REMOTE_ADDR");
	SetItem("REMOTE_HOST");
	SetItem("REMOTE_PORT");
	SetItem("REMOTE_USER");
	SetItem("REQUEST_METHOD");
	SetItem("SCRIPT_NAME");
	SetItem("SCRIPT_TRANSLATED");
	SetItem("SERVER_NAME");
	SetItem("SERVER_PORT");
	SetItem("SERVER_PORT_SECURE");
	SetItem("SERVER_PROTOCOL");
	SetItem("SERVER_SOFTWARE");
	SetItem("SSI_EXEC_DISABLED");
	SetItem("UNENCODED_URL");
	SetItem("URL");
	SetItem("URL_PATH_INFO"); //Only available in IIS 5.0...don't use this variable, use PATH_INFO instead.

	//Chop up the ALL_HTTP server variable to loop through
	// the HTTP request headers and add them to the Wsgi environment.
	string httpHeaders = Context->GetServerVariable("ALL_HTTP");

	//Split the headers, they should be separated by cr/lf's.
	stringstream headersStream(httpHeaders);
	string nameValuePair;
	while(getline(headersStream, nameValuePair)) {
		string name = nameValuePair.substr(0, nameValuePair.find_first_of(':'));
	
		//Exceptions for the wsgi validator.
		if (name != "HTTP_CONTENT_TYPE" && name != "HTTP_CONTENT_LENGTH") {
			SetItem(name.c_str());
		}
	}

	//Exceptions for the wsgi validator.
	SetItem("HTTP_CONTENT_TYPE", "CONTENT_TYPE");
	SetItem("HTTP_CONTENT_LENGTH", "CONTENT_LENGTH");

	//Set the Wsgi-specific variables.
	Python_ptr wsgiVersion(Py_BuildValue("(ii)", 1, 0)); //Make a tuple.
	PyDict_SetItemString(PythonEnvironment, "wsgi.version", wsgiVersion.get());

	//Add in the url scheme.
	string Https = Context->GetServerVariable("HTTPS");
	Python_ptr urlScheme;
	if(Https == "1" || Https == "on") {
		urlScheme = Python_ptr(PyString_FromString("https"));
	} else {
		urlScheme = Python_ptr(PyString_FromString("http"));
	}
	PyDict_SetItemString(PythonEnvironment, "wsgi.url_scheme", urlScheme.get());

	//Add the input stream.
	Python_ptr inputStream(_PyObject_New(&InputStreamType));
	InputStreamObject* wsgiInputStream = (InputStreamObject*)inputStream.get();
	wsgiInputStream->Request = Context->Request;
	PyDict_SetItemString(PythonEnvironment, "wsgi.input", inputStream.get());

	//Add the error stream.
	Python_ptr errorStream(_PyObject_New(&ErrorStreamType));
	PyDict_SetItemString(PythonEnvironment, "wsgi.errors", errorStream.get());

	//Yes, we're multithreaded.
	Python_ptr wsgiMultithreaded(PyBool_FromLong(1));
	PyDict_SetItemString(PythonEnvironment, "wsgi.multithread", wsgiMultithreaded.get());

	//No, we're not multiprocess.
	Python_ptr wsgiMultiprocess(PyBool_FromLong(0));
	PyDict_SetItemString(PythonEnvironment, "wsgi.multiprocess", wsgiMultiprocess.get());

	//We're not CGI, so no run_once for us...kinda the point.
	Python_ptr wsgiRunOnce(PyBool_FromLong(0));
	PyDict_SetItemString(PythonEnvironment, "wsgi.run_once", wsgiRunOnce.get());
}

void HttpResponse::SetItem(const string& variableName) {
	SetItem(variableName, variableName);
}

void HttpResponse::SetItem(const string& variableName, const string& alias) {
	string itemValue = Context->GetServerVariable(variableName);
	if(itemValue.length() > 0) {
		Python_ptr stringValue (PyString_FromString(itemValue.c_str()));
		if(0 != PyDict_SetItemString(PythonEnvironment, alias.c_str(), stringValue.get())) {
			string errorMessage = PythonApiHelpers::GetErrorMessage();
			throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage);
		}
	}
}

PyObject* HttpResponse::CompileScriptToModule(const string& scriptPath, const string& virtualPath) {
	FILE* fp;

	Py_BEGIN_ALLOW_THREADS

	int accessResult = _access_s(scriptPath.c_str(), 04);
	if(0 != accessResult) {
		ostringstream errorMessage;
		
		errorMessage << "An error occurred accessing resource (" << virtualPath.c_str();
		errorMessage << "). Error number (" << accessResult;
		errorMessage << ").";
		
		if(accessResult == EACCES) {
			//401...Unauthorized.
			throw HttpException(HTTP_STATUS_DENIED, "UNAUTHORIZED", errorMessage.str());
		} else if(accessResult == ENOENT) {
			//404...Not Found.
			throw HttpException(HTTP_STATUS_NOT_FOUND, "NOT FOUND", errorMessage.str());
		} else {
			//500...Server said WTF?!?
			throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage.str());
		}
	}

	errno_t result = fopen_s(&fp, scriptPath.c_str(), "r");
	if(0 != result) {
		ostringstream errorMessage;
		errorMessage << "Unable to open file(" << virtualPath.c_str();
		errorMessage << "). Error number(" << result;
		errorMessage << ").";

		throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage.str());
	}
	Py_END_ALLOW_THREADS

	struct _node* parsedCodeObject = PyParser_SimpleParseFile(fp, virtualPath.c_str(), Py_file_input);
	fclose(fp);
	fp = NULL;

	if(!parsedCodeObject) {
		string pythonErrorMessage = PythonApiHelpers::GetErrorMessage();
		
		string errorMessage;
		if(pythonErrorMessage.length() > 0) {
			errorMessage.append("\r\n");
			errorMessage.append(pythonErrorMessage);
		} else {
			errorMessage = "Unexpected error occurred parsing input file.";
		}

		throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage);
	}

	//PyNode_Compile() is the equivalent of the Python built-in function, compile().
	//It comes from the Python header node.h.
	Python_ptr compiledCodeObject((PyObject*)PyNode_Compile(parsedCodeObject, virtualPath.c_str()));
	PyNode_Free(parsedCodeObject);
	parsedCodeObject = NULL;

	if(!compiledCodeObject) {
		string pythonErrorMessage = PythonApiHelpers::GetErrorMessage();
		if(pythonErrorMessage.length() <= 0) {
			pythonErrorMessage = "An unknown exception occurred compiling the Python script.";
		}

		throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", pythonErrorMessage);
	}

	string moduleName;
	try {
		//The name of the module is generated through MD5 hash
		//off the actual wsgi file path.
		//This ensures that wsgi modules cannot overwrite one another from separate
		//threads, and also ensures that invalid characters don't end up
		// in the module name.
		MD5HashProvider hasher;
		hasher.AddData(scriptPath);
		moduleName = hasher.GetHashString();	
	} catch(runtime_error& error) {
		string errorMessage(error.what());
		throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage);
	}

	PyObject* module = NULL;
	module = PyImport_ExecCodeModuleEx((LPSTR)moduleName.c_str(), compiledCodeObject.get(), (LPSTR)virtualPath.c_str());

	if(!module) {
		string errorMessage = PythonApiHelpers::GetErrorMessage();

		throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage);
	}
	
	//mod_wsgi adds a "__mtime__" to the module at this point.
	//Maybe for script change detection?
	//TODO: Cache a wsgi script module, and don't go recompiling
	// it if you find it has already been compiled, and the file hasn't changed.
	return module;
}

void HttpResponse::ExecuteWsgiScript(const string& filePath, const string& virtualPath) {
	GILock lock; //Aquire the GIL.
	{
		Python_ptr compiledModule(CompileScriptToModule(filePath, virtualPath));
		Python_ptr wsgiCallable(PyObject_GetAttrString(compiledModule.get(), "application")); //TODO: Make the WSGI entry point name user configurable.
		
		if(!wsgiCallable.get()) {
			string errorMessage = PythonApiHelpers::GetErrorMessage();

			throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage);
		} else if (!PyCallable_Check(wsgiCallable.get())) {
			string errorMessage("Wsgi entry point must be callable.");

			throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage);
		}

		BuildWsgiEnvironment();

		PyObject* globalModuleDict = PyImport_GetModuleDict(); //Borrowed reference, do not decref.
		PyObject* responseModule = PyDict_GetItemString(globalModuleDict, WsgiResponseModule); //Borrowed reference, do not decref.
		Python_ptr start(PyObject_GetAttrString(responseModule, "start_response"));

		Python_ptr args(Py_BuildValue("(OO)", PythonEnvironment, start.get()));
		Python_ptr result(PyObject_Call(wsgiCallable.get(), args.get(), NULL));

		if(!result) {
			string errorMessage = PythonApiHelpers::GetErrorMessage();

			throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage); 
		}

		Python_ptr wsgiIterator;
		if(PyIter_Check(result.get())) {
			wsgiIterator = result;
		} else {
			wsgiIterator = Python_ptr(PyObject_GetIter(result.get()));
			if(!PyIter_Check(wsgiIterator.get())) {
				string errorMessage("Wsgi entry point must return something iterable.");
				throw HttpException(HTTP_STATUS_SERVER_ERROR, "INTERNAL SERVER ERROR", errorMessage);
			}
		}

		//Output to the client.
		PyObject* item = NULL;
		while(item = PyIter_Next(wsgiIterator.get())) {
			string outputItem;
			if(PyString_Check(item)) {
				outputItem = string(PyString_AsString(item));	
			} else {
				//According to PEP 333, the caller shouldn't be sending me
				// iterables with non-string elements...but I know that won't
				// ALWAYS be true, so lets at least not crash IIS.
				Python_ptr stringRepresentation(PyObject_Str(item));
				outputItem = string(PyString_AsString(stringRepresentation.get()));
			}

			Write(outputItem);
			Py_DECREF(item);
		}
	} //This scope guarantees Python_ptr's get destroyed before the GIL releases.
}

string HttpResponse::get_ContentType() const {
	NameValueCollection headers = *Headers;
	return *headers["content-type"];
}

void HttpResponse::set_ContentType(const string& contentType) {
	NameValueCollection headers = *Headers;
	headers["content-type"] = string_ptr(new string(contentType));
}

string HttpResponse::get_Status() const {
	return Status;
}

void HttpResponse::set_Status(const string& status) {
	Status = status;
}

tr1::shared_ptr<NameValueCollection> HttpResponse::get_Headers() const {
	return Headers;
}

void HttpResponse::set_ContentLength(ULONGLONG contentLength) {
	ostringstream contentLengthStream;
	contentLengthStream << contentLength;

	string contentLengthString = contentLengthStream.str();

	(*Headers)["content-length"] = string_ptr(new string(contentLengthString));
}

ULONGLONG HttpResponse::get_ContentLength() const {
	ULONGLONG returnValue;
	istringstream contentLengthStream( (*(*Headers)["content-length"]) );
	
	contentLengthStream >> returnValue;

	return returnValue;
}


void HttpResponse::SendHeaders() {
	if(!HeadersSent) {
		NameValueCollection::iterator headerIterator;

		//Iterate the headers map and build a string
		// containing http headers.
		ostringstream headers;
		for(headerIterator = Headers->begin(); headerIterator != Headers->end(); headerIterator++) {
			headers << headerIterator->first;
			headers << ": ";
			headers << *headerIterator->second;
			headers << "\r\n";
		}

		headers << "\r\n";

		string hdr(headers.str().c_str());

		string status = get_Status();

		HSE_SEND_HEADER_EX_INFO info;
		info.pszStatus = status.c_str();
		info.cchStatus = status.length();
		info.pszHeader = hdr.c_str();
		info.cchHeader = headers.str().length();
		info.fKeepConn = TRUE;

		Context->ECB->ServerSupportFunction(Context->ECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &info, NULL, NULL);

		HeadersSent = TRUE;
	}
}

//This function is called from the Python interpreter.
//Return all exceptions THROUGH the Python interpreter using
// PyErr_* functions and return NULL to indicate failure to the interpreter.
PyObject* HttpResponse::start_response(PyObject* args) {
	LPCSTR status = NULL;
	int statusLength = 0;
	PyObject* responseHeaders = NULL;
	PyObject* exc_info = NULL;

	if(!PyArg_ParseTuple(args, "s#O|O:start_response", &status, &statusLength, &responseHeaders, &exc_info)) {
		//PyArg_ParseTuple() raises its own exceptions, so
		// just return NULL and forget it.
		return NULL;
	}

	if(!PyList_Check(responseHeaders)) {
		//responseHeaders should be a Python List of Tuples.
		PyErr_SetString(PyExc_TypeError, "Headers argument should be a List.");

		return NULL;
	}

	//exc_info is an argument sent to us by the WSGI client
	// when the client encounters an error.
	if(exc_info && exc_info != Py_None) {
		//Handling an error when exc_info is provided.
		
		//Make sure exec_info is a sys.exec_info() tuple.		
		if(!PyTuple_Check(exc_info)) {
			PyErr_SetString(PyExc_RuntimeError, "Argument exc_info is invalid. It should be the tuple returned by sys.exec_info().");
			return NULL;
		}

		//It should have 3 elements in it.
		Py_ssize_t tupleSize = PyTuple_GET_SIZE(exc_info);
		if(tupleSize != 3) {
			PyErr_SetString(PyExc_RuntimeError, "Argument exc_info is invalid. It should be the tuple returned by sys.exec_info().");
			return NULL;
		}

		if(HeadersSent) {
			//PEP 333 says I should try to re-raise the exception
			// at this point if I can.
			Python_ptr exc_type(PyTuple_GET_ITEM(exc_info, 0));
			Python_ptr exc_value(PyTuple_GET_ITEM(exc_info, 1));
			Python_ptr exc_traceback(PyTuple_GET_ITEM(exc_info, 2));

			if(!exc_type.get() && exc_type.get() != Py_None) {
				PyErr_Restore(exc_type.get(), exc_value.get(), exc_traceback.get());
			} else {
				PyErr_SetString(PyExc_RuntimeError, "An unknown exception has occurred processing this request.");
			}

			return NULL;
		}
	} else if(HeadersSent) {
		PyErr_SetString(PyExc_RuntimeError, "Function start_response() cannot be called as headers have already been sent to the client.");

		return NULL;
	}

	string httpStatus(status, statusLength);
	//PEP 333 calls for ensuring the http status
	// is sent to the client with no control chars, leading/trailing spaces,
	// stuff like that.  It seems IIS is pretty good at fixing the status field
	// all by itself.
	set_Status(httpStatus);
	
	try {
		StoreHeaders(responseHeaders);
	} catch(runtime_error& err) {
		PyErr_SetString(PyExc_RuntimeError, err.what());

		return NULL;
	}

	PyObject* ModuleDict = PyImport_GetModuleDict(); //Borrowed reference, do not decref.
	PyObject* ResponseModule = PyDict_GetItemString(ModuleDict, WsgiResponseModule); //Borrowed reference, do not decref.

	return PyObject_GetAttrString(ResponseModule, "write");
}

void HttpResponse::StoreHeaders(PyObject* responseHeaders) {
	Headers->clear();

	if(!PyList_Check(responseHeaders)) {
		throw runtime_error("HTTP Response headers must be a List.");
	}

	//Write the response headers to the holding location...don't
	// send them to the client yet.
	
	Py_ssize_t listSize = PyList_GET_SIZE(responseHeaders);
	for(Py_ssize_t index = 0; index < listSize; index++) {
		//Borrowed reference, do not DECREF
		PyObject* headerItem = PyList_GET_ITEM(responseHeaders, index);

		//Skip non-tuple entries, or entries that are not
		// a name-value pair.
		if(PyTuple_Check(headerItem) && PyTuple_GET_SIZE(headerItem) == 2) {
			//Borrowed reference, to not DECREF
			PyObject* headerName = PyTuple_GET_ITEM(headerItem, 0);
			//Borrowed reference, to not DECREF
			PyObject* headerValue = PyTuple_GET_ITEM(headerItem, 1);

			//Skip headers that have extra-bogus header names.
			if(!(headerName && PyString_Check(headerName))) {
				continue;
			}
			
			if(!headerValue || Py_None == headerValue) {
				continue;
			}

			Python_ptr betterHeaderValue(PyObject_Str(headerValue));

			//TODO: Should also validate there are no control chars
			// coming in from the script on the header value.
			string key(PyString_AsString(headerName));

			static regex invalidCharRemover(REGEX_RESPONSE_HEADER_NAME_CLEANER);
			key = regex_replace(key, invalidCharRemover, "");

			//Skip headers that have nothing in they name portion
			// after cleaning.
			if(key.length() > 0) {
				string_ptr value(new string(PyString_AsString(betterHeaderValue.get())));	
				(*Headers)[key] = value;
			}
		}
	}
}

PyObject* HttpResponse::write(PyObject* args) {
	LPCSTR responseData = NULL;
	int responseDataLength = 0;

	if(!PyArg_ParseTuple(args, "s:write", &responseData, &responseDataLength)) {
		//PyArg_ParseTuple will have already set error state.	
		return NULL;
	}

	string responseString(responseData, responseDataLength);
	Write(responseString);

	//write() doesn't really return anything.
	Py_INCREF(Py_None);
	return Py_None;
}

void HttpResponse::Write(const string& content) {
	SendHeaders();

	DWORD length = content.length();
	Context->ECB->WriteClient(Context->ECB->ConnID, (LPVOID)content.c_str(), &length, NULL);
}

void HttpResponse::End() {
	SendHeaders();

	//Tell IIS that we're done processing the request.
	DWORD status = HSE_STATUS_SUCCESS_AND_KEEP_CONN;
	Context->ECB->ServerSupportFunction(Context->ECB->ConnID, HSE_REQ_DONE_WITH_SESSION, &status, NULL, NULL);
}